iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
Software Development

Android 鐵人賽: 天命最高 - 陪伴大家一步步打造屬於自己的app系列 第 20

Android Activity 資料傳遞實戰:從飲料點餐到會員註冊,掌握 Intent 雙向回傳

  • 分享至 

  • xImage
  •  

前一天我們已經開始接觸 Android 的 Intent,知道如何從一個 Activity 跳到另外一個 Activity。

但是實際開發 App 時,通常不只是「跳過去」這麼簡單,而是:

MainActivity → 第二個 Activity → 使用者操作 → 把資料帶回 MainActivity

今天就利用課堂上的幾個實作,把 IntentputExtra()setResult()ActivityResultLauncherSharedPreferences 串在一起,最後完成一個簡易的 會員註冊+登入+飲料甜點點餐系統

本日使用的程式包含 DrinkActivity、飲料選擇版 MainActivityRegisterActivityOrderActivity,以及另一個「城市 → 美食」Activity 回傳練習。


📱 今天要學什麼?

今天的重點可以濃縮成一張流程圖:

┌──────────────────────┐
│     MainActivity     │
│                      │
│  選擇 / 輸入資料       │
└──────────┬───────────┘
           │
           │ Intent
           │ putExtra()
           ▼
┌──────────────────────┐
│   Second Activity    │
│                      │
│ 使用者選擇 / 輸入資料   │
└──────────┬───────────┘
           │
           │ setResult()
           │ Intent
           ▼
┌──────────────────────┐
│     MainActivity     │
│                      │
│ 接收結果並更新畫面      │
└──────────────────────┘

也就是:

Activity A 不只可以把資料送給 Activity B,Activity B 做完事情之後,也可以把結果再傳回 Activity A。

這正是今天最重要的觀念。


一、先從最簡單的飲料選擇開始 ☕🥤

紅茶

可樂

第一個範例的流程非常適合理解 Activity Result:

MainActivity
     │
     │ 點擊「選擇飲料」
     ▼
DrinkActivity
     │
     ├── 輸入飲料
     ├── 選擇甜度
     └── 選擇冰塊
     │
     │ setResult()
     ▼
MainActivity
     │
     ├── 飲料名稱
     ├── 甜度
     └── 冰塊

MainActivity 中,程式使用新版的 ActivityResultLauncher<Intent>

private lateinit var getResult: ActivityResultLauncher<Intent>

接著透過:

registerForActivityResult(
    ActivityResultContracts.StartActivityForResult()
)

建立結果接收器。


二、ActivityResultLauncher:等待另一個 Activity 回傳結果

MainActivity 的核心程式如下:

getResult = registerForActivityResult(
    ActivityResultContracts.StartActivityForResult()
) { result ->

    Log.d("main", "code = ${result.resultCode}")

    if (result.resultCode == drinkCode) {

        val intent = result.data

        if (intent != null) {

            val drink = intent.getStringExtra("drink")
            val sugar = intent.getStringExtra("sugar")
            val ice = intent.getStringExtra("ice")

            tv_drink.text = getString(
                R.string.drink_label,
                drink
            )

            tv_sugar.text = getString(
                R.string.sugar_label,
                sugar
            )

            tv_ice.text = getString(
                R.string.ice_label,
                ice
            )
        }
    }
}

這裡的觀念很重要:

result.resultCode
        ↓
判斷是哪一種結果

result.data
        ↓
取得回傳 Intent

getStringExtra()
        ↓
把資料取出

也就是第二個 Activity 回來時,我們不只是知道「它關閉了」,還可以知道它帶了什麼資料回來。


三、如何開啟 DrinkActivity?

以前可能會寫:

startActivity(intent)

但今天因為我們需要等待結果回來,所以改成:

btn_select.setOnClickListener {

    val intent = Intent(
        this,
        DrinkActivity::class.java
    )

    getResult.launch(intent)
}

這裡:

getResult.launch(intent)

就代表:

開啟 DrinkActivity,而且我還在等它把結果送回來。


四、DrinkActivity:選飲料、甜度與冰塊

進入 DrinkActivity 後,程式取得:

val etDrink = findViewById<EditText>(R.id.et_drink)
val rgSugar = findViewById<RadioGroup>(R.id.rg_sugar)
val rgIce = findViewById<RadioGroup>(R.id.rg_ice)
val btnFinish = findViewById<Button>(R.id.btn_finish)

使用者可以輸入飲料名稱,再透過 RadioGroup 選擇甜度與冰塊。

按下完成之後:

btnFinish.setOnClickListener {

    val drink = etDrink.text.toString()

    val sugar =
        findViewById<RadioButton>(
            rgSugar.checkedRadioButtonId
        ).text.toString()

    val ice =
        findViewById<RadioButton>(
            rgIce.checkedRadioButtonId
        ).text.toString()

    val intent = Intent()

    intent.putExtra("drink", drink)
    intent.putExtra("sugar", sugar)
    intent.putExtra("ice", ice)

    setResult(drinkCode, intent)

    finish()
}

這裡可以看到今天另外兩個主角:

setResult()
finish()

五、putExtra():把資料裝進 Intent

假設使用者選擇:

飲料:紅茶
甜度:半糖
冰塊:少冰

程式就會:

intent.putExtra("drink", drink)
intent.putExtra("sugar", sugar)
intent.putExtra("ice", ice)

可以把 Intent 想像成一個包裹:

Intent
┌───────────────────────────┐
│ "drink" → "紅茶"          │
│ "sugar" → "半糖"          │
│ "ice"   → "少冰"          │
└───────────────────────────┘

然後:

setResult(drinkCode, intent)

把這個包裹交還給上一個 Activity。

最後:

finish()

結束目前的 DrinkActivity


六、進階實戰:會員註冊+登入+點餐

接下來的範例就更接近真正的 App。

整個程式有三個主要 Activity:

                ┌─────────────────┐
                │  MainActivity   │
                │                 │
                │ Name            │
                │ Email           │
                │ Password        │
                └───────┬─────────┘
                        │
              ┌─────────┴─────────┐
              │                   │
        Register             Login OK
              │                   │
              ▼                   ▼
┌────────────────────┐   ┌────────────────────┐
│ RegisterActivity   │   │   OrderActivity    │
│                    │   │                    │
│ Name               │   │ 飲料               │
│ Email              │   │ 甜點               │
│ Password           │   │ 數量               │
└─────────┬──────────┘   └─────────┬──────────┘
          │                        │
          │ setResult(200)         │ setResult(100)
          │                        │
          └──────────┬─────────────┘
                     ▼
              ┌──────────────┐
              │ MainActivity │
              │ 顯示結果      │
              └──────────────┘

這次同一個 MainActivity 必須處理兩種結果:

private val registerCode = 200
private val orderCode = 100

所以我們可以利用不同的 resultCode 判斷到底是哪一個 Activity 回來的。


七、RegisterActivity:會員資料回傳

RegisterActivity 要求輸入:

Name
Email
Password

按下 OK 時,先判斷三個欄位是不是都有資料:

if (
    editTextName.text.isNotEmpty() &&
    editTextEmail.text.isNotEmpty() &&
    editTextPassword.text.isNotEmpty()
) {

    val name = editTextName.text.toString()
    val email = editTextEmail.text.toString()
    val password = editTextPassword.text.toString()

    val resultIntent = Intent()

    resultIntent.putExtra("name", name)
    resultIntent.putExtra("email", email)
    resultIntent.putExtra("password", password)

    setResult(registerCode, resultIntent)

    finish()

} else {

    Toast.makeText(
        this,
        "Please fill all fields",
        Toast.LENGTH_SHORT
    ).show()
}

這裡再次使用相同模式:

取得資料
   ↓
Intent()
   ↓
putExtra()
   ↓
setResult()
   ↓
finish()

八、MainActivity 接收註冊資料

RegisterActivity 回來後:

if (result.resultCode == registerCode) {

    result.data?.run {

        val name = getStringExtra("name")
        val email = getStringExtra("email")
        val password = getStringExtra("password")

        tv_result.text = "Registered successfully!"

        txt_name.setText(name)
        txt_email.setText(email)
        txt_password.setText(password)
    }
}

這樣註冊頁面輸入的資料,就重新回到了 MainActivity。


九、SharedPreferences:把會員資料暫存起來

今天這份範例又多了一個很實用的東西:

SharedPreferences

取得 SharedPreferences:

val sp = getSharedPreferences(
    "userdata",
    MODE_PRIVATE
)

寫入資料:

sp.edit().apply {

    putString("name", name)
    putString("email", email)
    putString("password", password)

    apply()
}

所以會員註冊完成之後,資料除了顯示在畫面,也會存進:

userdata
├── name
├── email
└── password

接下來就可以拿來做簡易登入驗證。


十、登入驗證

當使用者按下 Order:

val inputName = txt_name.text.toString()
val inputPassword = txt_password.text.toString()

先取得使用者目前輸入的帳號密碼。

再讀取 SharedPreferences:

val sp = getSharedPreferences(
    "userdata",
    MODE_PRIVATE
)

val spName = sp.getString(
    "name",
    "none"
)

val spPassword = sp.getString(
    "password",
    "none"
)

最後比較:

if (
    inputName == spName &&
    inputPassword == spPassword
) {

    val orderIntent = Intent(
        this@MainActivity,
        OrderActivity::class.java
    )

    getResult.launch(orderIntent)

} else {

    Toast.makeText(
        this@MainActivity,
        "The name & password are wrong",
        Toast.LENGTH_SHORT
    ).show()
}

登入成功才可以進入點餐畫面。


十一、今天的大魔王:OrderActivity 🍹🍨

今天最好玩的地方就是這個點餐系統。

程式把飲料分成兩組 Menu。

Menu 1

可樂

紅茶

拿鐵

第一組:

🥤 Cola    $50
🍵 Tea     $60
☕ Coffee $100

程式:

rbCola.text = "Cola , $50"
ivDrink1.setImageResource(R.drawable.cola_1)

rbTea.text = "Tea , $60"
ivDrink2.setImageResource(R.drawable.blacktea)

rbCoffee.text = "Coffee , $100"
ivDrink3.setImageResource(R.drawable.latte)

十二、Menu 2

冰淇淋

奶茶

牛奶

按下第二組選單後變成:

🍨 Ice cream $150
🧋 Milk tea  $100
🥛 Milk       $80

程式:

rbCola.text = "Ice cream , $150"
ivDrink1.setImageResource(R.drawable.icecream)

rbTea.text = "Milk tea , $100"
ivDrink2.setImageResource(R.drawable.milk_tea_icon)

rbCoffee.text = "Milk , $80"
ivDrink3.setImageResource(R.drawable.milk_icon)

因此我們不用另外建立第二個 Activity,只要動態修改:

RadioButton 文字
+
ImageView 圖片
+
價格計算方式

就能讓同一個畫面切換不同 Menu。


十三、updateMenu():把重複程式整理成函式

這裡我覺得是一個很值得記下來的寫法:

private fun updateMenu(menuNum: Int) {

    rgDrinks.clearCheck()

    drinkNum = menuNum

    if (menuNum == 1) {

        rbCola.text = "Cola , $50"
        ivDrink1.setImageResource(R.drawable.cola_1)

        rbTea.text = "Tea , $60"
        ivDrink2.setImageResource(R.drawable.blacktea)

        rbCoffee.text = "Coffee , $100"
        ivDrink3.setImageResource(R.drawable.latte)

    } else {

        rbCola.text = "Ice cream , $150"
        ivDrink1.setImageResource(R.drawable.icecream)

        rbTea.text = "Milk tea , $100"
        ivDrink2.setImageResource(R.drawable.milk_tea_icon)

        rbCoffee.text = "Milk , $80"
        ivDrink3.setImageResource(R.drawable.milk_icon)
    }
}

按鈕就只需要:

btnDrink1.setOnClickListener {
    updateMenu(1)
}

btnDrink2.setOnClickListener {
    updateMenu(2)
}

比起把整段程式複製兩次,這種方式更容易維護。


十四、RadioButton 與 CheckBox 的差別

這個點餐畫面也剛好把 Android 常見的兩種選擇元件放在一起。

RadioButton

飲料使用:

○ Cola
○ Tea
○ Coffee

搭配:

RadioGroup

代表:

三種飲料中只能選一種。

CheckBox

甜點則使用:

☑ Waffle
☑ Pancake
☑ Muffin

代表:

可以一次選很多種。

所以簡單記:

RadioButton → 單選
CheckBox   → 複選

這也是 Android 表單介面非常常見的設計。


十五、TextWatcher:輸入數量後自動勾選

這次還加入了一個我覺得很方便的小功能。

例如使用者直接在 Waffle 數量輸入:

2

程式就自動:

☑ Waffle

不用再自己點 CheckBox。

這裡使用的是:

TextWatcher

程式:

val dessertQtyWatcher = {
        editText: EditText,
        checkBox: CheckBox ->

    object : TextWatcher {

        override fun beforeTextChanged(
            s: CharSequence?,
            start: Int,
            count: Int,
            after: Int
        ) {
        }

        override fun onTextChanged(
            s: CharSequence?,
            start: Int,
            before: Int,
            count: Int
        ) {
        }

        override fun afterTextChanged(
            s: Editable?
        ) {

            val qty =
                s.toString().toIntOrNull() ?: 0

            checkBox.isChecked = qty >= 1
        }
    }
}

接著:

etWaffleQty.addTextChangedListener(
    dessertQtyWatcher(
        etWaffleQty,
        cbWaffle
    )
)

etPancakeQty.addTextChangedListener(
    dessertQtyWatcher(
        etPancakeQty,
        cbPancake
    )
)

etMuffinQty.addTextChangedListener(
    dessertQtyWatcher(
        etMuffinQty,
        cbMuffin
    )
)

因此:

輸入 0
  ↓
☐ 不勾選

輸入 1
  ↓
☑ 自動勾選

輸入 2
  ↓
☑ 自動勾選

十六、開始計算訂單金額 💰

假設選:

Cola × 2

程式:

val unitPrice =
    if (drinkNum == 1) 50
    else 150

val name =
    if (drinkNum == 1) "Cola"
    else "Ice cream"

val price = unitPrice * drinkQty

total += price

如果:

Cola = $50
數量 = 2

就是:

50 × 2 = 100

再把結果加入 StringBuilder

summary.append(
    "Drink : $name -> $$unitPrice x $drinkQty = $price\n"
)

十七、甜點也一起計算

例如 Waffle:

if (cbWaffle.isChecked) {

    val qty =
        etWaffleQty.text
            .toString()
            .toIntOrNull() ?: 0

    val price = 100 * qty

    total += price

    summary.append(
        "Waffle -> $100 x $qty = $price\n"
    )
}

Pancake:

if (cbPancake.isChecked) {

    val qty =
        etPancakeQty.text
            .toString()
            .toIntOrNull() ?: 0

    val price = 120 * qty

    total += price

    summary.append(
        "Pancake -> 120 x $qty = $price\n"
    )
}

Muffin:

if (cbMuffin.isChecked) {

    val qty =
        etMuffinQty.text
            .toString()
            .toIntOrNull() ?: 0

    val price = 80 * qty

    total += price

    summary.append(
        "Muffin -> 80 x $qty = $price\n"
    )
}

最後:

summary.append(
    "\nThe total fee is $$total"
)

就完成一個簡單的購物車計價功能。


十八、結帳後把整張訂單傳回 MainActivity

最重要的是,OrderActivity 並不是算完就結束。

它還會:

val data = tvResult.text.toString()

val intent = Intent()

intent.putExtra(
    "order",
    data
)

setResult(
    orderCode,
    intent
)

finish()

也就是:

OrderActivity
      │
      │ 訂單內容
      │
      │ putExtra("order", data)
      ▼
   Intent
      │
      │ setResult(100)
      ▼
MainActivity

MainActivity 再判斷:

else if (
    result.resultCode == orderCode
) {

    val data =
        result.data
            ?.getStringExtra("order")

    tv_result.text = data
}

於是整份訂單又回到了首頁。


十九、另外一個練習:城市 → 美食推薦 🍜

今天的程式資料裡還有另一個很適合練習 Activity 傳值的範例。

MainActivity 先讓使用者選:

○ 台北
○ 新北
○ 桃園

取得選取的 RadioButton:

val selectedId =
    rgCity.checkedRadioButtonId

if (selectedId != -1) {

    val radioButton =
        findViewById<RadioButton>(
            selectedId
        )

    val cityName =
        radioButton.text.toString()

    val intent =
        Intent(
            this,
            CityActivity::class.java
        )

    intent.putExtra(
        "CITY_NAME",
        cityName
    )

    startActivityForResult(
        intent,
        citycode
    )
}

二十、CityActivity 根據城市動態改變內容

CityActivity 收到:

val cityName =
    intent.getStringExtra(
        "CITY_NAME"
    ) ?: ""

接著利用:

when (cityName)

判斷城市。

例如台北:

"台北" -> {

    cbFood1.text = "牛肉麵"
    cbFood2.text = "滷肉飯"
    cbFood3.text = "小籠包"
    cbFood4.text = "芒果冰"

    cbFood1.setCompoundDrawablesWithIntrinsicBounds(
        0,
        0,
        R.drawable.taipei_food1,
        0
    )

    cbFood2.setCompoundDrawablesWithIntrinsicBounds(
        0,
        0,
        R.drawable.taipei_food2,
        0
    )

    cbFood3.setCompoundDrawablesWithIntrinsicBounds(
        0,
        0,
        R.drawable.taipei_food3,
        0
    )

    cbFood4.setCompoundDrawablesWithIntrinsicBounds(
        0,
        0,
        R.drawable.taipei_food4,
        0
    )
}

同一個 Activity 就可以依據不同城市,動態更換文字與圖片。


二十一、把選好的美食送回首頁

使用者勾選想吃的東西:

val selectedFoods =
    mutableListOf<String>()

if (cbFood1.isChecked)
    selectedFoods.add(
        cbFood1.text.toString()
    )

if (cbFood2.isChecked)
    selectedFoods.add(
        cbFood2.text.toString()
    )

if (cbFood3.isChecked)
    selectedFoods.add(
        cbFood3.text.toString()
    )

if (cbFood4.isChecked)
    selectedFoods.add(
        cbFood4.text.toString()
    )

然後:

val resultIntent = Intent()

resultIntent.putExtra(
    "CITY_NAME",
    cityName
)

resultIntent.putExtra(
    "SELECTED_FOODS",
    selectedFoods.joinToString(", ")
)

setResult(
    citycode,
    resultIntent
)

finish()

最後 MainActivity 顯示:

tvResult.text =
    "城市名稱:$cityName\n" +
    "美食建議:$selectedFoods"

這就是完整的雙向 Activity 資料傳遞。


二十二、新舊兩種 Activity Result 寫法

今天的範例剛好同時出現兩種寫法。

舊式寫法

startActivityForResult(
    intent,
    citycode
)

接:

override fun onActivityResult(
    requestCode: Int,
    resultCode: Int,
    data: Intent?
) {
    super.onActivityResult(
        requestCode,
        resultCode,
        data
    )
}

城市美食範例使用的就是這個方式。

新式寫法

private lateinit var getResult:
    ActivityResultLauncher<Intent>

搭配:

getResult =
    registerForActivityResult(
        ActivityResultContracts
            .StartActivityForResult()
    ) { result ->

        // 處理回傳結果
    }

啟動:

getResult.launch(intent)

飲料以及會員點餐範例採用這個方式。

所以今天最大的收穫之一,就是實際看到了:

舊:
startActivityForResult()
        +
onActivityResult()

             ↓

新:
registerForActivityResult()
        +
ActivityResultLauncher

二十三、今天完整資料流

把今天全部整理成一張圖:

                    Android App
                         │
                         ▼
                 ┌──────────────┐
                 │ MainActivity │
                 └──────┬───────┘
                        │
          ┌─────────────┴──────────────┐
          │                            │
          ▼                            ▼
 ┌─────────────────┐          ┌─────────────────┐
 │RegisterActivity │          │  OrderActivity  │
 └────────┬────────┘          └────────┬────────┘
          │                            │
          │ name                       │ drink
          │ email                      │ dessert
          │ password                   │ quantity
          │                            │ total
          ▼                            ▼
      Intent                        Intent
          │                            │
          │ setResult(200)             │ setResult(100)
          └─────────────┬──────────────┘
                        ▼
                 ┌──────────────┐
                 │ MainActivity │
                 └──────┬───────┘
                        │
                 SharedPreferences
                        │
              ┌─────────┴─────────┐
              │                   │
            name               password

這時候已經不再只是單純的「Activity 跳頁」,而是開始具備一個 App 真正需要的頁面流程、狀態與資料傳遞


二十四、今日重點整理

今天一次練到了不少 Android 開發的重要觀念:

Intent
 │
 ├── Activity 跳轉
 │
 ├── putExtra()
 │      └── 傳送資料
 │
 ├── getStringExtra()
 │      └── 取得資料
 │
 ├── setResult()
 │      └── 回傳資料
 │
 └── finish()
        └── 結束 Activity

Activity Result
 │
 ├── startActivityForResult()
 │
 └── ActivityResultLauncher

UI
 │
 ├── RadioGroup
 ├── RadioButton
 ├── CheckBox
 ├── EditText
 ├── ImageView
 ├── TextView
 └── Toast

資料處理
 │
 ├── StringBuilder
 ├── MutableList
 ├── TextWatcher
 └── SharedPreferences

從飲料選擇、城市美食,到最後的會員註冊與點餐系統,其實都是同一個核心概念:

不同 Activity 各自負責不同工作,再透過 Intent 交換資料。


🎬 今日課程操作影片

  1. Android 實作影片

結語

Day 20 已經開始從「Android 元件怎麼用」,慢慢進入「一個 App 的功能怎麼串起來」。

以前看到:

Intent(this, SecondActivity::class.java)

可能只覺得 Intent 就是拿來換頁。

但今天實作之後,可以看到 Intent 真正有趣的地方是:

Activity A
   ↓
傳資料
   ↓
Activity B
   ↓
處理資料
   ↓
回傳結果
   ↓
Activity A

再搭配 SharedPreferences,就可以開始保存一些簡單的使用者資料。

今天完成的會員+點餐範例,已經有:

註冊 → 儲存資料 → 登入驗證 → 選擇商品 → 輸入數量 → 自動計價 → 回傳訂單

從最基本的 Activity 切換,一步一步組合之後,就已經開始有真正 App 的味道了!

Yeshttps://ithelp.ithome.com.tw/upload/images/20260820/20112100zxRjh1wWFT.png


上一篇
Intent 不只會跳頁:Activity 資料傳遞、回傳結果與多國語系實戰
下一篇
Android 選單大集合:Menu、Dialog、WebView 到 Navigation,一次搞懂 App 的操作入口
系列文
Android 鐵人賽: 天命最高 - 陪伴大家一步步打造屬於自己的app21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言